데이터 중심 애플리케이션 설계 | 1장 신뢰할 수 있고 확장 가능하며 유지보수하기 쉬운 애플리케이션

데이터 중심 애플리케이션 설계 | 1장 신뢰할 수 있고 확장 가능하며 유지보수하기 쉬운 애플리케이션

날짜
Mar 27, 2024
태그
data
설명
데이터 중심 애플리케이션 설계를 뒷받침하는 근본 개념

개요

지난 10년 간 데이터베이스와 분산 시스템 분야는 여러 흥미로운 발전이 있었다. 우리가 알만한 인터넷 기업은 엄청난 양의 데이터와 트래픽을 다룬다. 이러한 발전을 가져온 원동력은 매우 다양하다.
  • 큰 규모의 데이터 및 트래픽을 효율적으로 처리하기 위해 새로운 도구를 만들어야 했다.
  • 기업은 상황에 맞춰 민첩하고 적은 노력을 들여 테스트 하고 빠르게 대처해야 한다. 이는 개발 주기를 단축하고 데이터 모델을 유연하게 해야 가능하다.
  • CPU의 속도는 거의 증가하지 않고 있고, 멀티코어 프로세서가 표준이 됐고, 네트워크는 계속 빨라지고 있다. 즉, 병렬처리는 계속 늘고 있다.
  • AWS와 같은 서비스형 인프라 덕택에 소규모 팀에서도 여러 장비에 분산된 시스템을 개발할 수 있고, 구축이 가능하다.
  • 많은 사람들이 고가용성을 찾는다. 더는 정전, 유지보수 때문에 서비스가 중단되는 것을 이해하지 못한다.
  • 오늘날 많은 애플리케이션은 계산 중심적(CPU)보단 데이터 중심적(데이터)이다.
 
급격한 기술 변화에도 변하지 않는 원리가 있다. 이 원리를 이해하면 각 도구가 적합한 장소와 도구를 활용하는 방법, 도구에 숨어있는 함정을 피하는 방법을 알 수 있다. 시스템 내부의 동작 방식과 직관을 기른다면 시스템 동작을 추론할 수 있고, 올바른 설계를 위한 의사결정이 가능하고 발생 가능한 모든 문제를 추적할 수 있다.
 
이런 궁금증이 있다면 이 책은 유용하다.
  • 웹, 모바일 앱이 수백만 명의 사용자를 감당할 수 있게 하고 싶다.
  • 고가용성을 갖추고 싶다. 중단 시간을 최소화 하는 방법을 알고 싶다.
  • 온라인 서비스의 내부가 어떻게 돌아가는지 알고 싶다.
 
이러한 궁금증을 가진 사람들에게 이렇게 이야기 하곤 한다. “우린 구글이나 아마존이 아니잖아. 관계형 데이터베이스로도 충분해.” 작업에 걸맞는 도구를 선택하는 것도 중요하다. 그래서 관계형 데이터는 물론 중요하다. 하지만 관계형 데이터베이스가 데이터를 다루기 위한 마지막 보루는 아니라는 점을 앞으로 설명한다.
 

신뢰성, 확장성, 유지보수성을 갖춘 애플리케이션

최신 애플리케이션은 이러한 필요를 가진다.
  • 언제나 다시 데이터를 찾을 수 있는 데이터베이스를 가지고 있다.
  • 읽기 속도 향상을 위해 수행 결과를 기억하는 캐시를 활용한다.
  • 사용자가 키워드를 통한 검색을 하거나 필터링을 할 수 있도록 검색과 색인을 제공한다.
  • 비동기 처리를 위한 메시지를 활용한다.
  • 주기적으로 대량의 누적된 데이터를 분석(일괄처리)한다.
 
이러한 필요는 사용자에게 출발했다. 신뢰성, 확장성, 유지보수성의 의미를 명확히 아는 것이 이번 장의 목표이다.
 

데이터 시스템에 대한 생각

일반적으로 우리가 아는 데이터 시스템 도구는 데이터베이스, 큐, 캐시 등이 있다. 모두 다른 범주에 속하는 도구로 생각하지만, 매우 다른 접근 패턴을 가지고 있어 구현 방식이 매우 다르다. 데이터베이스와 메시지 큐는 표면적으로 비슷하더라도(데이터를 저장하는 행위) 다른 접근 방법을 가지고 있는 것 처럼 말이다.
 
새로운 도구가 생겨나고, 이는 사용자의 요구에 맞게 최적화되었기 때문에 더 이상 전통적인 분류에 들어맞지 않는다. 분류간 경계가 흐려지고 있다. 더 이상 단일 도구로는 더 이상 데이터 처리와 저장 모두를 만족시키기 어려운 광범위한 요구 사항을 가지고 있다.
 
다양한 구성 요소를 결합한 데이터 시스템 아키텍처의 예
다양한 구성 요소를 결합한 데이터 시스템 아키텍처의 예
서비스 제공을 위해 각 도구를 결합한 데이터시스템을 만들었다. 유저는 일관된 결과를 얻지만, 내부적으로 어떻게 동작하고 있는지 알지 못한다. 유저의 요구는 그저 읽기, 쓰기, 색인과 같은 기능을 빠르고 오류없이 얻어내기를 원한다.
 
개발자는 각 도구를 결합하는 데이터시스템의 설계자이기도 하다.
  • 내부적인 문제가 생겨도 데이터를 정확하고 완전하게 유지하려면 어떻게 해야할까?
  • 시스템 일부 성능이 저하되더라도 유저에게 일관되게 좋은 성능을 제공하기 위해 어떻게 해야할까?
  • 부하를 다루기 위해 어떻게 규모를 조절해야 할까?
  • 서비스를 위한 좋은 API는 어떻게 작성할까?
 

신뢰성

물리적인 결함, 인적 오류 같은 상황에도 시스템은 지속적으로 올바르게 동작해야 한다. 무언가 잘못되더라도 지속적으로 올바르게 동작한다는 것을 우리는 ‘신뢰성’이라고 이야기 할 수 있다.
 
일반적으로 소프트웨어에 기대하는 바는 다음과 같다.
  • 사용자가 기대한 기능을 수행한다.
  • 사용자의 실수나 예상치 못한 사용법을 허용할 수 있다.
  • 예상된 부하와 데이터 양을 충분히 이겨낼 수 있다.
  • 허가되지 않은 접근과 오남용을 방지한다.
 
결함은 장애와 동일하지 않다. 결함은 사양에서 벗어난 시스템의 한 구성 요소라 하면, 장애는 사용자에게 필요한 서비스를 제공하지 못하고 시스템 전체가 멈춘 상태다.
 

하드웨어 결함

하드디스크의 고장, 대규모 정전 사태, 누군가 케이블을 잘못 뽑는 것을 우린 결함이라고 말한다. 이러한 결함을 방지하기 위해 하드웨어 구성 요소에 중복을 추가하는 경우가 대부분이다. RAID 디스크 구성, 이중 전원 디바이스와 핫 그왑 가능한 CPU, 예비 전원용 디젤 발전기를 갖출 수 있다. 이러한 방법으로 완전히 장애를 막을 순 없지만 보통 계속 동작할 수 있게 해준다. 이러한 시스템은 운영상의 장점이 있다.
 

소프트웨어 오류

보통 하드웨어 결함은 무작위적이고 서로 독립적이다. 물리적으로 분리되어 장비의 결함이 다른 장비에 영향을 미치지 않는다. 하드웨어와 비교해 소프트웨어의 결함은 예상하기 어렵고, 노드 간 상간관계 때문에 시스템 오류를 더 많이 유발하는 경향이 있다.
  • 잘못된 특정 입력으로 인한 모든 서버 인스턴스가 죽는 소프트웨어 버그.
  • CPU 시간, 메모리, 디스크 공간, 네트워크 대역폭처럼 공유 자원을 과도하게 사용하는 일부 프로세스.
  • 시스템의 속도가 느려져 반응이 없거나 잘못된 응답을 반환하는 프로세스.
  • 한 구성 요소의 작은 결함으로 시작해 다른 구성 요소의 결함을 발생시키는 연쇄 장애.
 
이러한 버그들은 특정 상황이 발생하기 전까지 오랫동안 나타나지 않는다. 소프트웨어의 오류는 신속한 해결책이 없다. 깊게 생각하고, 빈틈없이 테스트를 만들고, 프로세스를 격리하고, 죽은 프로세스를 살리고, 프로덕션 환경의 동작을 추적하고 측정하고 분석한다.
 

인적 오류

최선의 의도를 가지고 있어도 사람은 실수를 한다. 사람이 미덥지 않음에도 시스템을 어떻게 신뢰도 있게 만들까? 최고의 시스템은 다양한 접근 방식을 결합한다.
  • 잘 설계된 추상화, API, 관리 인터페이스를 사용
  • 실제 사용자에게 영향이 없는 비 프로덕션 샌드박스를 구축
  • 단위 테스트, 통합 테스트, 수동 테스트
  • 쉬운 롤백, 데이터 재계산 도구를 제공
  • 성능과 오류를 확인할 수 있는 모니터링 도구를 제공
  • 조직 교육과 실습을 시행
 

확장성

데이터, 트래픽의 양과 복잡성이 증가함에 따라 이를 처리할 수 있는 적절한 방법이 있어야 한다. 현재 안정적으로 동작하는 소프트웨어가 미래에도 안정적으로 동작한다는 보장은 없다. 흔한 이유 중 하나는 부하의 증가다. 사용자 수가 1만 명에서 10만, 100만 명으로 증가한다면 시스템은 전에 처리한 양보다 더 많은 데이터를 처리해야 한다.
 

부하 기술하기

부하는 몇 가지 항목의 숫자로 나타낼 수 있다.
  • 웹 서버의 초당 요청 수
  • DB의 읽기 대 쓰기 비율
  • 동시 활성 사용자 수
  • 캐시 적중율
 
좀 더 직관적인 이해를 위해 트위터의 사례를 가져왔다. 해당 자료는 12년 11월에 공개된 데이터를 기반으로 한다.
트위터는 크게 두 가지 동작을 한다.
  • 트윗 작성: 사용자는 팔로워에게 메시지를 게시할 수 있다(평균 4.6k 요청, 피크 시 12k 요청)
  • 홈 타임라인 보기: 사용자는 팔로우한 사람이 작성한 트윗을 볼 수 있다(초당 300k 요청)
 
단순 초당 12,000건의 쓰기 작업 처리는 간단하다. 하지만 트위터의 확장성 문제는 주로 트윗의 양이 아닌 팬아웃(fan-out) 떄문이다. 개별 사용자는 많은 사람을 팔로우 하고 많은 사람이 개별 사용자를 팔로우 한다. 이 두 가지 동작을 구현하는 방법은 크게 두 가지다.
 
트윗 작성, 홈 타임라인 보기의 동작을 구현하는 두 가지 방법
트윗 작성, 홈 타임라인 보기의 동작을 구현하는 두 가지 방법
 
  1. 쓰기 작업 < 읽기작업 트윗 작성은 새로운 트윗을 전역 컬렉션에 삽입한다. 사용자가 자신의 홈 타임라인을 요청하면 팔로우하는 모든 사람을 찾고, 이 사람들의 모든 트윗을 찾아 시간순으로 정렬하여 합친다.
    1. SELECT tweet.*, users FROM tweets JOIN users ON tweets.sender_id = users.id JOIN follows ON follows.sollowee_id = users.id WHERE follows.follower_id = current_user
      그림 1-2와 같은 관계형DB 쿼리문
 
  1. 쓰기 작업 > 읽기 작업 개별 사용자의 홈 타임라인 캐시를 유지한다. 사용자가 트윗을 작성하면 해당 사용자를 팔로우하는 사람을 모두 찾아 팔로워 각자의 타임라인 캐시에 새로운 트윗을 삽입한다. 미리 쓰기 작업을 했기 때문에 홈 타임라인의 읽기 요청은 비용이 비교적 저렴해진다.
 
팔로워가 많으면 쓰기 작업에 많은 리소스가 들어간다. 보통의 경우는 팔로워가 많으면 1번 방법을, 적으면 2번 방법을 채택하여 사용한다. 하지만, 사용자의 팔로워 분포에 따라 부하의 비율이 달라지기 떄문에 확장성을 다르게 고려해야 하는 상황도 생길 수 있다. 대부분 사용자의 트윗은 계속해서 사람들이 작성할 때 홈 타임라인에 펼쳐지지만 팔로워 수가 많은 소수의 사용자(유명인)는 팬아웃에서 제외된다. 사용자가 팔로우한 유명인의 트윗은 별도로 가져와 접근 방식 1처럼 읽는 시점에 사용자의 홈 타임라인에 합친다. 이러한 혼합형 접근 방식은 좋은 성능으로 지속적인 전송이 가능하다.
 

성능 기술하기

부하 매개변수를 증가시키고 시스템 자원은 변경하지 않고 유지하면 시스템 성능은 어떻게 영향을 받을까? 부하 매개 변수를 증가시켰을때 성능이 변하지 않고 유지되길 원한다면 자원을 얼마나 많이 늘려야 할까?
 
모두 성능 수치가 필요하다.
  • 일괄 처리 시스템은 처리량에 관심을 가진다.
  • 온라인 시스템은 서비스의 응답 시간에 관심을 가진다.
 
💡
지연 시간과 응답 시간 응답 시간은 클라이언트 관점에서 본 시간으로 요청을 처리하는 시간 외에도 네트워크 지연과 큐 지연도 포함한다. 지연 시간은 요청이 처리되길 기다리는 시간으로 서비스를 기다리는 상태인 시간을 말한다.
 
서비스에 대한 100건의 요청에 대한 응답 시간
서비스에 대한 100건의 요청에 대한 응답 시간
 
클라이언트가 동일한 요청을 하더라도 매번 응답 시간이 다르다. 응답 시간은 단일 숫자가 아니라 측정 가능한 값의 분포이다. 대부분의 요청은 꽤 빠르지만, 가끔 오래 걸리는 특이 값이 있다. 모든 요청에 동일한 시간이 걸려야 한다고 생각할 수 있다. 백그라운드 프로세스의 컨텍스트 스위치, 네트워크 패킷 손실과 TCP 재전송, 가비지 컬렉션 휴지, 디스크에서 읽기를 강제하는 페이지 폴트, 서버랙의 기계적인 진동이나 다른 여러 원인으로 추가 지연이 생길 수 있다.
 
평균 응답 시간은 일반적이지만, 하지만 평균은 그다지 좋은 지표는 아니다. 얼마나 많은 사용자가 실제로 지연을 경험했는지 알려주지 않기 때문이다. 평균보다는 백분위를 사용하는 편이 더 좋다. 응답 시간 목록을 가지고 가장 빠른 시간부터 제일 느린 시간까지 정렬하면 중간 지점이 중앙값이 된다. 사용자가 보통 얼마나 오랫동안 기다려야 하는지 알고 싶다면 중앙값이 좋은 지표이다. 특이 값이 얼마나 좋지 않은지 알아보려면 상위 백분위를 살펴보는 것도 좋다.
 
큐 대기 지연은 높은 백분위에서 응답 시간의 상당 부분을 차지한다. 서버는 병렬로 소수의 작업만 처리할 수 있기 때문에 소수의 느린 요청 처리만으로 후속 요청 처리가 지체된다. 이러한 현상을 선두 차단 현상이라고 한다.
 
단 하나의 느린 백엔드 요청이 전체 최종 사용자 요청을 느리게 한다.
단 하나의 느린 백엔드 요청이 전체 최종 사용자 요청을 느리게 한다.
 

부하 대응 접근 방식

부하 매개변수가 어느 정도 증가하더라도 좋은 성능을 유지하려면 어떻게 해야 할까? 급성장하는 서비스를 맡고 있다면 부하 규모의 자릿수가 바뀔 때마다 혹은 그보다 자주 아키텍처를 재검토해야 할지 모른다.
 
확장성과 관련한 구분
용량 확장(scaling up)
더 강력한 장비로 이동
규모 확장(scaling out)
다수의 장비에 부하를 분산
단일 장비로 수행될 수 있는 시스템은 간단하지만 고사양 장비는 매우 비싸기 때문에 대개 규모 확장을 피하지 못한다. 현실적으로 좋은 아키텍처는 실용적인 접근 방식의 조합이 필요하다.
 
일부 시스템은 탄력적이다. 부하 증가를 감지하면 컴퓨팅 자원을 알아서 추가할 수 있다. 단일 노드에 상태 유지(stateful) 데이터 시스템을 분산 설치하는 일은 아주 많은 복잡도가 추가적으로 발생한다. 확장 비용이나 데이터 베이스를 분산으로 만들어야 하는 고가용성 요구가 있을 때까지 단일 노드에 데이터베이스를 유지하는 것(용량 확장)이 최근까지의 통념이다.
 
범용적이고 모든 상환에 맞는 마법의 확장 아키텍처는 없다. 아키텍처를 결정하는 요소는 읽기의 양, 쓰기의 양, 저장할 양의 데이터, 데이터의 복잡도, 응답 시간 요구사항, 접근 패턴 등이 있다.
 

유지보수성

모든 사용자(개발자 혹은 운영자)가 시스템 안에서 생산적으로 작업할 수 있는 환경을 만들어야 한다.
 
유지보수에는 버그 수정, 시스템 운영 유지, 장애 조사, 새로운 플랫폼 적응, 새 사용 사례를 위한 변경, 기술 채무 상환, 새로운 기능 추가 등이 있다.
 
유지보수의 고통을 최소하하고 레거시 소프트웨어를 직접 만들지 않게끔 설계할 수 있는 세가지 원칙이 있다.
  • 운용성: 운영팀이 시스템을 원활하게 운영할 수 있도록 쉽게 만들자
  • 단순성: 시스템의 복잡도를 최대한 제거해 새로운 엔지니어가 시스템을 이해하기 쉽게 만들자
  • 발전성: 이후에 시스템을 쉽게 변경할 수 있게 하자. 유연성, 수정가능성, 적응성으로 알려져 있다.
 

운용성: 운영의 편리함 만들기

아무리 좋은 소프트웨어라도 나쁘게 운영할 경우 작동을 신뢰할 수 없다. 시스템이 지속해서 원활하게 작동하려면 운영팀이 필수다. 좋은 운영팀은 다음과 같은 작업을 책임진다.
  • 상태를 모니터링하고, 좋지 않다면 빠르게 서비스를 복원
  • 시스템 장애, 성능 저하 등의 문제를 추적
  • 보안 패치를 포함한 소프트웨어와 플랫폼을 최신 상태로 유지
  • 시스템 간 의존성 관리
  • 미래에 발생할 문제를 예측해 미리 해결
  • 배포, 설정 관리 등을 위한 모범 사례외 도구를 마련
  • 애플리케이션을 다른 플랫폼으로 이동하는 등 복잡한 유지보수 태스크를 진행
  • 설정 변경으로 인한 시스템 보안 유지
  • 예측 가능한 운영과 안정적인 서비스 환경을 유지하기 위한 절차를 강의
  • 인사 이동에도 시스템에 대한 조직의 지식을 보존함
 

단순성: 복잡도 관리

복잡도가 커진 소프트웨어 프로젝트를 커다란 진흙 덩어리로 묘사한다. 복잡도는 다양한 증상으로 나타난다.
  • 상태 공간의 급증
  • 모듈 간 강한 커플링
  • 복잡한 의존성
  • 일관성 없는 네이밍과 용어
  • 성능 문제 해결을 목표로 한 해킹
  • 임시 방편으로 문제를 해결한 특수 사례
 
복잡도가 높으면 시스템 유지보수의 예산과 일정이 증가한다. 그리고 버그가 생길 위험도 커진다. 복잡도를 줄이는 최상의 도구는 추상화다. 좋은 추상화는 깔끔하고 직관적인 외관 아래로 많은 세부 구현을 숨길 수 있다.
 
고수준의 프로그래밍 언어는 기계 언어, CPU 레지스터, 시스템 호출을 숨긴 추상화이다. SQL은 디스크에 기록하고 메모리에 저장한 복잡한 데이터 구조와 다른 클라이언트의 동시 요청과 고장 후 불일치를 숨긴 추상황이다. 단지 직접 사용하지 않을 뿐이다. 추상화 덕분에 기계어를 생각할 필요가 없다.
 
추상화는 큰 시스템의 일부를, 잘 정의되고 재사용 가능한 구성 요소로 추출할 수 있게 한다.
 

발전성: 변화를 쉽게 만들기

시스템의 요구사항이 영원히 바뀌지 않을 가능성은 매우 적다. 요구 사항은 계속해서 변한다. 데이터 시스템 변경을 쉽게 하고 변화된 요구사항에 시스템을 맞추는 방법은 시스템의 간단함과 추상화와 밀접한 관련이 있다.
 

댓글

guest